﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace MMOEng.World
{
    public class MultiLODHeightmap : BaseObject
    {
        public int TotalWidth { get; private set; }
        public int TotalHeight { get; private set; }

        public int TilesWide { get; private set; }
        public int TilesHigh { get; private set; }

        public int LevelsOfDetail { get; private set; }

        public Matrix World { get; set; }
        public Matrix View { get; set; }
        public Matrix Projection { get; set; }

        private string wholeMapImagePath;

        /// <summary>
        /// The height of highest vertex on the heightmap
        /// </summary>
        private float highestPeak;

        /// <summary>
        /// The heighest that any point of the height map (a completely white pixel) can get
        /// </summary>
        public static readonly float MaxHeightForVertex = 100.0f;
        /// <summary>
        /// The space in between elements of rows or columns of vertices. 
        /// </summary>
        public static readonly float CellSize = 5.0f;

        /// <summary>
        /// Data structure for a single level of detail for a single section of the MultiLODHeightmap
        /// </summary>
        private class heightMap
        {
            public byte[,] mapData;
            public int mapWidth;
            public int mapHeight;
            /// <summary>
            /// Create a single level of detail heightmap.
            /// </summary>
            /// <param name="width">Width of map</param>
            /// <param name="height">Height of map</param>
            public heightMap(int width,int height){ this.mapData = new byte[width,height]; this.mapWidth = width; this.mapHeight = height; }
        }

        private class multiLODHeightMap
        {
            public int levels;
            public heightMap[] lodData;
            /// <summary>
            /// Create a new height map with numerous levels-of-detail
            /// </summary>
            /// <param name="levels">Number of levels that this height map has. Higher numbers mean less detail in the higher levels</param>
            /// <param name="width">Width of heightmap</param>
            /// <param name="height">Height of heightmap</param>
            public multiLODHeightMap(int levels, int width, int height)
            {
                this.levels = levels; this.lodData = new heightMap[levels];
                for (int i = 0; i < levels; i++) { this.lodData[i] = new heightMap(width, height); }
            }
        }

        /// <summary>
        /// A two dimensional array containg every level of detail for each tiled section of the overall heightmap
        /// </summary>
        private multiLODHeightMap[,] mapTiles;

        private Texture2D[] SplitTextureIntoTiles(Texture2D tex, int tilesWide, int tilesHigh)
        {
            //Check that the size of each tile is an integer
            int texWidth = tex.Width; int texHeight = tex.Height;
            if ((texWidth % tilesWide) != 0 || (texHeight % tilesHigh) != 0)
                throw new InvalidOperationException("Trying to divide a texture into non integer sized pieces");

            //Initialize tile parameters
            Texture2D[] allTiles = new Texture2D[tilesWide * tilesHigh];
            int tileWidth = tex.Width / tilesWide; int tileHeight = tex.Height / tilesHigh;

            for (int y = 0; y < tilesHigh; y++)
            {
                for (int x = 0; x < tilesWide; x++)
                {
                    //Initialize new texture
                    allTiles[x + (y * tilesWide)] = new Texture2D(game.GraphicsDevice, tileWidth, tileHeight);
                    //grab data from large texture
                    Color[] data = new Color[tileWidth * tileHeight];
                    int index = (x*tileWidth) + ((y*tileHeight)*texWidth);
                    tex.GetData<Color>(0, new Rectangle(x * tileWidth, y * tileHeight, tileWidth, tileHeight), data, index, tileWidth * tileHeight);
                    //copy data into tile's texture
                    allTiles[x + (y * tilesWide)].SetData<Color>(data);
                }
            }
            return allTiles;
        }
        
        /// <summary>
        /// Generate a single level of detail from a section height map image
        /// </summary>
        /// <param name="sectionImage"></param>
        /// <returns></returns>
        private multiLODHeightMap BuildMultiLODHeightmapFromImage(Texture2D sectionImage, int levels)
        {  
            //get heightmap size from image size
            int w = sectionImage.Width; int h = sectionImage.Height;

            //Create a new multi level container
            multiLODHeightMap hm = new multiLODHeightMap(levels, w, h);

            //make lod start at 0 instead of 1
            levels++;

            //convert image to datamap
            byte[,] imgData = BuildDataMapFromTexture(sectionImage);

            //initialize and build each level
            for (int lod = 0; lod < levels; lod++)
            {
                //calculate size of section based on its level of detail
                int divisor = (int)Math.Pow(2.0d, (double)lod);
                int newWidth = sectionImage.Width / divisor;
                int newHeight = sectionImage.Height / divisor;

                //make new map
                heightMap map = new heightMap(newWidth, newHeight);

                //skip through the raw data and downsample to the current level of detail
                for (int y = 0; y < newHeight; y++)
                {
                    for (int x = 0; x < newWidth; x++)
                    {
                        map.mapData[x, y] = imgData[x * divisor, y * divisor];
                    }
                }
            }

            return hm;
        }

        /// <summary>
        /// Convert a Texture2D into an 2d array of floats containing the white/black value for each pixel
        /// </summary>
        /// <param name="image">Texure2D to convert</param>
        /// <returns>An array of values equal to how white each pixel is</returns>
        private byte[,] BuildDataMapFromTexture(Texture2D image){
            //Init map
            int w = image.Width; int h = image.Height;
            byte[,] output = new byte[w, h];

            //init color buffer
            Color[] buff = new Color[w * h];

            //fill buffer
            image.GetData<Color>(buff);

            //step through map and fill out data map
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    int index = x + (y * w);
                    Color c = buff[index];
                    byte heightVal = (byte)((c.R + c.G + c.B) / 3.0f);
                    //update highest peak val, if this vertex is higher
                    highestPeak = Math.Max(highestPeak,(heightVal/255.0f)*MaxHeightForVertex);
                    output[x, y] = heightVal;
                }
            }
            return output;   
        }

        /// <summary>
        /// Create a new height map by supplying the path of the complete heightmap image. This image represents the whole map, 
        /// not a single tile within. This image is divided int sub map based on the values for high and wide
        /// </summary>
        /// <param name="game">Root game object</param>
        /// <param name="totalMapPath">Path to complete heightmap image</param>
        /// <param name="wide">How many tiles to divide the map into along the x axis</param>
        /// <param name="high">How many tiles to divide the map into along the y axis</param>
        /// <param name="levelsofdetail">Set number of steps in the LOD system</param>
        public MultiLODHeightmap(MMOGame game, string totalMapPath, int wide, int high, int levelsofdetail)
            : base(game)
        {
            TilesHigh = high;
            TilesWide = wide;
            LevelsOfDetail = levelsofdetail;

            //remember image path
            this.wholeMapImagePath = totalMapPath;

            //reset highest vertex
            highestPeak = 0.0f;

            //init map tiles
            mapTiles = new multiLODHeightMap[wide,high];
        }

        /// <summary>
        /// Update this heightmap with each game tick
        /// </summary>
        /// <param name="gameTime">Timing parameters for currently running game</param>
        public override void Update(GameTime gameTime)
        {
        }

        /// <summary>
        /// Draw this heightmap with the current camera's view and projection matrices
        /// </summary>
        /// <param name="gameTime">Game timing snapshot</param>
        public override void  Draw(GameTime gameTime)
        {
        }

        /// <summary>
        /// Load all required content and initialize most values
        /// </summary>
        public override void  LoadContent()
        {
            //Load texture
            Texture2D largeTex = game.ResourceManager.GetResource<Texture2D>(wholeMapImagePath);

            //remember entire map dimensions
            TotalHeight = largeTex.Height;
            TotalWidth = largeTex.Width;

            //Split large texture
            Texture2D[] tilesTex = this.SplitTextureIntoTiles(largeTex,TilesWide,TilesHigh);
            
            //Build a heightmap for each
            for (int y = 0; y < TilesHigh; y++)
            {
                for (int x = 0; x < TilesWide; x++)
                {
                    mapTiles[x, y] = this.BuildMultiLODHeightmapFromImage(tilesTex[x + (y * TilesWide)], LevelsOfDetail);
                }
            }
        }
        /// <summary>
        /// Get the smallest bounding box that contains every vertex of the entire heightmap
        /// </summary>
        /// <returns>Smallest possible bounding box</returns>
        public BoundingBox GetHeightMapBounds()
        {
            return new BoundingBox(new Vector3(0, 0, 0), new Vector3(TotalWidth * CellSize, highestPeak, TotalHeight * CellSize));
        }
    }
}
